<!DOCTYPE html>
<!--
Copyright (c) 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/math/range_utils.html">
<link rel="import" href="/tracing/base/math/statistics.html">
<link rel="import" href="/tracing/base/utils.html">
<link rel="import" href="/tracing/model/frame.html">

<script>
'use strict';

/**
 * @fileoverview Class for managing android-specific model meta data,
 * such as rendering apps, and frames rendered.
 */
tr.exportTo('tr.model.helpers', function() {
  const Frame = tr.model.Frame;
  const Statistics = tr.b.math.Statistics;

  const UI_DRAW_TYPE = {
    NONE: 'none',
    LEGACY: 'legacy',
    MARSHMALLOW: 'marshmallow'
  };

  const UI_THREAD_DRAW_NAMES = {
    'performTraversals': UI_DRAW_TYPE.LEGACY,
    'Choreographer#doFrame': UI_DRAW_TYPE.MARSHMALLOW
  };

  const RENDER_THREAD_DRAW_NAME = 'DrawFrame';
  const RENDER_THREAD_INDEP_DRAW_NAME = 'doFrame';
  const RENDER_THREAD_QUEUE_NAME = 'queueBuffer';
  const RENDER_THREAD_SWAP_NAME = 'eglSwapBuffers';
  const THREAD_SYNC_NAME = 'syncFrameState';

  function getSlicesForThreadTimeRanges(threadTimeRanges) {
    const ret = [];
    threadTimeRanges.forEach(function(threadTimeRange) {
      const slices = [];

      threadTimeRange.thread.sliceGroup.iterSlicesInTimeRange(
          function(slice) { slices.push(slice); },
          threadTimeRange.start, threadTimeRange.end);
      ret.push.apply(ret, slices);
    });
    return ret;
  }

  function makeFrame(threadTimeRanges, surfaceFlinger) {
    const args = {};
    if (surfaceFlinger && surfaceFlinger.hasVsyncs) {
      const start = Statistics.min(threadTimeRanges,
          function(threadTimeRanges) { return threadTimeRanges.start; });
      args.deadline = surfaceFlinger.getFrameDeadline(start);
      args.frameKickoff = surfaceFlinger.getFrameKickoff(start);
    }
    const events = getSlicesForThreadTimeRanges(threadTimeRanges);
    return new Frame(events, threadTimeRanges, args);
  }

  function findOverlappingDrawFrame(renderThread, uiDrawSlice) {
    if (!renderThread) return undefined;

    // of all top level renderthread slices, find the one that has a 'sync'
    // within the uiDrawSlice
    let overlappingDrawFrame;
    const slices = tr.b.iterateOverIntersectingIntervals(
        renderThread.sliceGroup.slices,
        function(range) { return range.start; },
        function(range) { return range.end; },
        uiDrawSlice.start,
        uiDrawSlice.end,
        function(rtDrawSlice) {
          if (rtDrawSlice.title === RENDER_THREAD_DRAW_NAME) {
            const rtSyncSlice = rtDrawSlice.findDescendentSlice(
                THREAD_SYNC_NAME);
            if (rtSyncSlice &&
                rtSyncSlice.start >= uiDrawSlice.start &&
                rtSyncSlice.end <= uiDrawSlice.end) {
              // sync observed which overlaps ui draw. This means the RT draw
              // corresponds to the UI draw
              overlappingDrawFrame = rtDrawSlice;
            }
          }
        });
    return overlappingDrawFrame;
  }

  /**
   * Builds an array of {start, end} ranges grouping common work of a frame
   * that occurs just before performTraversals().
   *
   * Only necessary before Choreographer#doFrame tracing existed.
   */
  function getPreTraversalWorkRanges(uiThread) {
    if (!uiThread) return [];

    // gather all frame work that occurs outside of performTraversals
    const preFrameEvents = [];
    uiThread.sliceGroup.slices.forEach(function(slice) {
      if (slice.title === 'obtainView' ||
          slice.title === 'setupListItem' ||
          slice.title === 'deliverInputEvent' ||
          slice.title === 'RV Scroll') {
        preFrameEvents.push(slice);
      }
    });
    uiThread.asyncSliceGroup.slices.forEach(function(slice) {
      if (slice.title === 'deliverInputEvent') {
        preFrameEvents.push(slice);
      }
    });

    return tr.b.math.mergeRanges(
        tr.b.math.convertEventsToRanges(preFrameEvents),
        3,
        function(events) {
          return {
            start: events[0].min,
            end: events[events.length - 1].max
          };
        });
  }

  function getFrameStartTime(traversalStart, preTraversalWorkRanges) {
    const preTraversalWorkRange =
      tr.b.findClosestIntervalInSortedIntervals(
          preTraversalWorkRanges,
          function(range) { return range.start; },
          function(range) { return range.end; },
          traversalStart,
          3);

    if (preTraversalWorkRange) {
      return preTraversalWorkRange.start;
    }
    return traversalStart;
  }

  function getRtFrameEndTime(rtDrawSlice) {
    // First try and get time that frame is queued:
    const rtQueueSlice = rtDrawSlice.findDescendentSlice(
        RENDER_THREAD_QUEUE_NAME);
    if (rtQueueSlice) {
      return rtQueueSlice.end;
    }
    // failing that, end of swapbuffers:
    const rtSwapSlice = rtDrawSlice.findDescendentSlice(
        RENDER_THREAD_SWAP_NAME);
    if (rtSwapSlice) {
      return rtSwapSlice.end;
    }
    // failing that, end of renderthread frame trace
    return rtDrawSlice.end;
  }

  function getUiThreadDrivenFrames(app) {
    if (!app.uiThread) return [];

    let preTraversalWorkRanges = [];
    if (app.uiDrawType === UI_DRAW_TYPE.LEGACY) {
      preTraversalWorkRanges = getPreTraversalWorkRanges(app.uiThread);
    }

    const frames = [];
    app.uiThread.sliceGroup.slices.forEach(function(slice) {
      if (!(slice.title in UI_THREAD_DRAW_NAMES)) {
        return;
      }

      const threadTimeRanges = [];
      const uiThreadTimeRange = {
        thread: app.uiThread,
        start: getFrameStartTime(slice.start, preTraversalWorkRanges),
        end: slice.end
      };
      threadTimeRanges.push(uiThreadTimeRange);

      // on SDK 21+ devices with RenderThread,
      // account for time taken on RenderThread
      const rtDrawSlice = findOverlappingDrawFrame(
          app.renderThread, slice);
      if (rtDrawSlice) {
        const rtSyncSlice = rtDrawSlice.findDescendentSlice(THREAD_SYNC_NAME);
        if (rtSyncSlice) {
          // Generally, the UI thread is only on the critical path
          // until the start of sync.
          uiThreadTimeRange.end = Math.min(uiThreadTimeRange.end,
              rtSyncSlice.start);
        }

        threadTimeRanges.push({
          thread: app.renderThread,
          start: rtDrawSlice.start,
          end: getRtFrameEndTime(rtDrawSlice)
        });
      }
      frames.push(makeFrame(threadTimeRanges, app.surfaceFlinger));
    });
    return frames;
  }

  function getRenderThreadDrivenFrames(app) {
    if (!app.renderThread) return [];

    const frames = [];
    app.renderThread.sliceGroup.getSlicesOfName(RENDER_THREAD_INDEP_DRAW_NAME)
        .forEach(function(slice) {
          const threadTimeRanges = [{
            thread: app.renderThread,
            start: slice.start,
            end: slice.end
          }];
          frames.push(makeFrame(threadTimeRanges, app.surfaceFlinger));
        });
    return frames;
  }

  function getUiDrawType(uiThread) {
    if (!uiThread) {
      return UI_DRAW_TYPE.NONE;
    }

    const slices = uiThread.sliceGroup.slices;
    for (let i = 0; i < slices.length; i++) {
      if (slices[i].title in UI_THREAD_DRAW_NAMES) {
        return UI_THREAD_DRAW_NAMES[slices[i].title];
      }
    }
    return UI_DRAW_TYPE.NONE;
  }

  function getInputSamples(process) {
    let samples = undefined;
    for (const counterName in process.counters) {
      if (/^android\.aq\:pending/.test(counterName) &&
        process.counters[counterName].numSeries === 1) {
        samples = process.counters[counterName].series[0].samples;
        break;
      }
    }

    if (!samples) return [];

    // output rising edges only, since those are user inputs
    const inputSamples = [];
    let lastValue = 0;
    samples.forEach(function(sample) {
      if (sample.value > lastValue) {
        inputSamples.push(sample);
      }
      lastValue = sample.value;
    });
    return inputSamples;
  }

  function getAnimationAsyncSlices(uiThread) {
    if (!uiThread) return [];

    const slices = [];
    for (const slice of uiThread.asyncSliceGroup.getDescendantEvents()) {
      if (/^animator\:/.test(slice.title)) {
        slices.push(slice);
      }
    }
    return slices;
  }

  /**
   * Model for Android App specific data.
   * @constructor
   */
  function AndroidApp(process, uiThread, renderThread, surfaceFlinger,
      uiDrawType) {
    this.process = process;
    this.uiThread = uiThread;
    this.renderThread = renderThread;
    this.surfaceFlinger = surfaceFlinger;
    this.uiDrawType = uiDrawType;

    this.frames_ = undefined;
    this.inputs_ = undefined;
  }

  AndroidApp.createForProcessIfPossible = function(process, surfaceFlinger) {
    let uiThread = process.getThread(process.pid);
    const uiDrawType = getUiDrawType(uiThread);
    if (uiDrawType === UI_DRAW_TYPE.NONE) {
      uiThread = undefined;
    }
    const renderThreads = process.findAllThreadsNamed('RenderThread');
    const renderThread = (renderThreads.length === 1 ?
      renderThreads[0] : undefined);

    if (uiThread || renderThread) {
      return new AndroidApp(process, uiThread, renderThread, surfaceFlinger,
          uiDrawType);
    }
  };

  AndroidApp.prototype = {
  /**
   * Returns a list of all frames in the trace for the app,
   * constructed on first query.
   */
    getFrames() {
      if (!this.frames_) {
        const uiFrames = getUiThreadDrivenFrames(this);
        const rtFrames = getRenderThreadDrivenFrames(this);
        this.frames_ = uiFrames.concat(rtFrames);

        // merge frames by sorting by end timestamp
        this.frames_.sort(function(a, b) { a.end - b.end; });
      }
      return this.frames_;
    },

    /**
     * Returns list of CounterSamples for each input event enqueued to the app.
     */
    getInputSamples() {
      if (!this.inputs_) {
        this.inputs_ = getInputSamples(this.process);
      }
      return this.inputs_;
    },

    getAnimationAsyncSlices() {
      if (!this.animations_) {
        this.animations_ = getAnimationAsyncSlices(this.uiThread);
      }
      return this.animations_;
    }
  };

  return {
    AndroidApp,
  };
});
</script>
